#!/usr/bin/env python
#
# Copyright 2009 Bret Taylor
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import logging
import ply.lex
import ply.yacc
import sys


def parse(file_name, value):
    LEXER.lineno = 1
    PARSER.file_name = file_name
    PARSER.num_errors = 0
    ast = PARSER.parse(value, lexer=LEXER)
    return ast if PARSER.num_errors == 0 else None


def print_ast(ast, name=None, indent=0):
    if isinstance(ast, list):
        if not name:
            name = ast[0]["node_pos"]["path"] if ast else "<base>"
        print "  " * indent, name
        for child in ast:
            print_ast(child, name, indent + 1)
    elif isinstance(ast, dict):
        children = sorted(k for k in ast.keys() if not k.startswith("node_"))
        print "  " * indent, ast["node_type"]
        for child in children:
            print_ast(ast[child], child, indent + 1)
    else:
        print "  " * indent, name + ":", repr(ast)


class Node(dict):
    def __init__(self, node_type=None, production=None, **kwargs):
        if production is None or node_type is None:
            frame = sys._getframe(1)
            if production is None:
                production = frame.f_locals["p"]
            if node_type is None:
                node_type = frame.f_code.co_name.split("_")[-1]
        if node_type != "constant":
            for name in kwargs.keys():
                if kwargs[name] is None: del kwargs[name]
        pos = dict(path=PARSER.file_name, line=production.lineno(1),
                   char=production.lexspan(1)[0])
        dict.__init__(self, node_type=node_type, node_pos=pos, **kwargs)

    def __getattr__(self, name):
        try:
            return self[name]
        except KeyError:
            raise AttributeError(name)


# Lexer

keywords = frozenset([
    "class",
    "extends",
    "async",
    "function",
    "null",
    "true",
    "false",
    "var",
    "for",
    "while",
    "if",
    "else",
    "in",
    "return",
    "break",
    "continue",
    "yield",
    "try",
    "catch",
])

tokens = [
    "identifier",
    "number",
    "string",
    "operator",
    "doublestar",
    "assignoperator",
    "incrementoperator",
] + list(k.upper() for k in keywords)

literals = "{}(),:;=.[]+-*/%!<>\|&^"

def t_comment(t):
    r'/\*(.|\n)*?\*/'
    t.lexer.lineno += t.value.count('\n')

def t_inline_comment(t):
    r'//.*'

def t_newline(t):
    r'\n+'
    t.lexer.lineno += t.value.count("\n")

def t_whitespace(t):
    r'\s+'

def t_identifier(t):
    r'[a-zA-Z_][a-zA-Z0-9_]*'
    if t.value in keywords:
        t.type = t.value.upper()
    return t

def t_number(t):
    r'\d+(.\d+)?'
    if "." in t.value:
        t.value = float(t.value)
    else:
        t.value = int(t.value)
    return t

def t_string(t):
    r'(\"[^\"\n]*\"|\'[^\'\n]*\')'
    t.value = t.value[1:-1]
    return t

def t_operator(t):
    r'==|!=|<=|>=|<<|>>|\|\||\&\&'
    return t

def t_incrementoperator(t):
    r'\+\+|\-\-'
    return t

def t_doublestar(t):
    r'\*\*'
    return t

def t_assignoperator(t):
    r'\+=|-=|\*=|\/=|\%=|<<=|>>='
    return t

def t_error(t):
    logging.error("%s:%d: Illegal character '%s'", PARSER.file_name,
                  t.lineno, t.value[0])
    PARSER.num_errors += 1
    t.lexer.skip(1)

LEXER = ply.lex.lex()


# Parser

precedence = (
    ('nonassoc', '<', '>', 'operator'),
    ('left', '+', '-'),
    ('left', '*', '/', '%'),
    ('left', '&', '|', '^'),
    ('right', 'unary_minus', '!'),
)

def p_file(p):
    """
    file : declaration_list
    """
    p[0] = p[1]

def p_declaration_list(p):
    """
    declaration_list : declaration_list function
                     | declaration_list class
                     | declaration_list variabledeclaration
                     |
    """
    if len(p) == 1:
        p[0] = []
    else:
        p[0] = p[1]
        p[0].append(p[2])

def p_class(p):
    """
    class : CLASS identifier '{' declaration_list '}'
    class : CLASS identifier EXTENDS identifier '{' declaration_list '}'
    """
    if len(p) == 6:
        p[0] = Node(name=p[2], members=p[4])
    else:
        p[0] = Node(name=p[2], extends=p[4], members=p[6])

def p_variabledeclaration(p):
    """
    variabledeclaration : VAR variable initial_value ';'
    """
    p[0] = Node(variable=p[2], value=p[3])

def p_function(p):
    """
    function : async FUNCTION identifier '(' parameters ')' type '{' statement_list '}'
             | async FUNCTION identifier '(' parameters ',' kwarg_parameters ')' type '{' statement_list '}'
             | async FUNCTION identifier '(' ')' type '{' statement_list '}'
             | async FUNCTION identifier '(' kwarg_parameters ')' type '{' statement_list '}'
    """
    if len(p) == 13:
        p[0] = Node(async=p[1], name=p[3], parameters=p[5], type=p[9],
                    body=p[11], **p[7])
    elif len(p) == 11:
        if isinstance(p[5], dict):
            p[0] = Node(async=p[1], name=p[3], parameters=[], type=p[7],
                        body=p[9], **p[5])
        else:
            p[0] = Node(async=p[1], name=p[3], parameters=p[5], type=p[7],
                        body=p[9])
    else:
        p[0] = Node(async=p[1], name=p[3], parameters=[], type=p[6], body=p[8])

def p_parameters(p):
    """
    parameters : parameters ',' variable initial_value
               | variable initial_value
    """
    if len(p) == 3:
        p[0] = [Node(variable=p[1], value=p[2], node_type="parameter")]
    else:
        p[0] = p[1]
        p[0].append(Node(variable=p[3], value=p[4], node_type="parameter"))

def p_initial_value(p):
    """
    initial_value : '=' expression
                  |
    """
    if len(p) > 1:
        p[0] = p[2]
    else:
        p[0] = None

def p_kwarg_parameters(p):
    """
    kwarg_parameters : '*' identifier
                     | doublestar identifier
                     | '*' identifier ',' doublestar identifier
    """
    if len(p) == 3:
        if p[1] == '*':
            p[0] = dict(args=p[2])
        else:
            p[0] = dict(kwargs=p[2])
    else:
        p[0] = dict(args=p[2], kwargs=p[5])

def p_async(p):
    """
    async : ASYNC
          |
    """
    p[0] = len(p) > 1

def p_variable(p):
    """
    variable : identifier type
    """
    p[0] = Node(name=p[1], type=p[2])

def p_statement_list(p):
    """
    statement_list : statement_list statement
                   |
    """
    if len(p) == 1:
        p[0] = []
    else:
        p[0] = p[1]
        p[0].append(p[2])

def p_statement(p):
    """
    statement : variabledeclaration
              | expressionstatement
              | assignment
              | for
              | while
              | if
              | return
              | break
              | continue
              | try
    """
    p[0] = p[1]

def p_expressionstatement(p):
    """
    expressionstatement : expression ';'
    """
    p[0] = Node(expression=p[1])

def p_assignment(p):
    """
    assignment : reference '=' expression ';'
               | reference assignoperator expression ';'
    """
    p[0] = Node(lvalue=p[1], operator=p[2], rvalue=p[3])

def p_for(p):
    """
    for : FOR '(' variable IN expression ')' '{' statement_list '}'
        | FOR '(' VAR variable IN expression ')' '{' statement_list '}'
    """
    if len(p) > 9:
        p[0] = Node(variable=p[4], expression=p[6], body=p[9],
                    declaration=True)
    else:
        p[0] = Node(variable=p[3], expression=p[5], body=p[8])

def p_while(p):
    """
    while : WHILE '(' expression ')' '{' statement_list '}'
    """
    p[0] = Node(expression=p[3], body=p[6])

def p_if(p):
    """
    if : IF '(' expression ')' '{' statement_list '}'
       | IF '(' expression ')' '{' statement_list '}' ELSE '{' statement_list '}'
    """
    if len(p) > 10:
        p[0] = Node(expression=p[3], body=p[6], else_body=p[10])
    else:
        p[0] = Node(expression=p[3], body=p[6])

def p_return(p):
    """
    return : RETURN ';'
           | RETURN expression ';'
    """
    if len(p) > 3:
        p[0] = Node(expression=p[2])
    else:
        p[0] = Node()

def p_break(p):
    """
    break : BREAK ';'
    """
    p[0] = Node()

def p_continue(p):
    """
    continue : CONTINUE ';'
    """
    p[0] = Node()

def p_try(p):
    """
    try : TRY '{' statement_list '}' catch_list
    """
    p[0] = Node(block=p[3], catches=p[5])

def p_catches(p):
    """
    catch_list : catch_list catch
               | catch
    """
    if len(p) == 2:
        p[0] = [p[1]]
    else:
        p[0] = p[1]
        p[0].append(p[2])

def p_catch(p):
    """
    catch : CATCH '(' variable ')' '{' statement_list '}'
    """
    p[0] = Node(variable=p[3], block=p[6])

def p_expression(p):
    """
    expression : reference
               | constant
               | unary
               | binary
               | call
               | anonymousfunction
               | comprehension
               | list
               | object
               | yield
               | '(' expression ')'
    """
    if len(p) == 2:
        p[0] = p[1]
    else:
        p[0] = p[2]

def p_expression_list(p):
    """
    expression_list : expression_list ',' expression
                    | expression
    """
    if len(p) == 1:
        p[0] = []
    elif len(p) == 2:
        p[0] = [p[1]]
    else:
        p[0] = p[1]
        p[0].append(p[3])

def p_constant(p):
    """
    constant : number
             | string
             | keyword_constant
    """
    p[0] = Node(value=p[1])

def p_keyword_constant(p):
    """
    keyword_constant : NULL
                     | TRUE
                     | FALSE
    """
    p[0] = dict(null=None, true=True, false=False)[p[1]]

def p_binary(p):
    """
    binary : expression '+' expression
           | expression '-' expression
           | expression '*' expression
           | expression '/' expression
           | expression '<' expression
           | expression '>' expression
           | expression '|' expression
           | expression '&' expression
           | expression '^' expression
           | expression '%' expression
           | expression operator expression
    """
    p[0] = Node(lhs=p[1], operator=p[2], rhs=p[3])

def p_unary(p):
    """
    unary : '!' expression
          | '-' expression %prec unary_minus
          | expression incrementoperator
    """
    if p[1] in ('!', '-'):
        p[0] = Node(operator=p[1], expression=p[2])
    else:
        p[0] = Node(operator=p[2], expression=p[1])

def p_call(p):
    """
    call : expression '(' expression_list ')'
         | expression '(' expression_list ',' kwarg_expressions ')'
         | expression '(' ')'
         | expression '(' kwarg_expressions ')'
    """
    if len(p) == 4:
        p[0] = Node(base=p[1], arguments=[])
    elif len(p) == 5:
        if isinstance(p[3], dict):
            p[0] = Node(base=p[1], arguments=[], **p[3])
        else:
            p[0] = Node(base=p[1], arguments=p[3])
    else:
        p[0] = Node(base=p[1], arguments=p[3], **p[5])

def p_kwarg_expressions(p):
    """
    kwarg_expressions : '*' expression
                      | doublestar expression
                      | '*' expression ',' doublestar expression
    """
    if len(p) == 3:
        if p[1] == '*':
            p[0] = dict(args=p[2])
        else:
            p[0] = dict(kwargs=p[2])
    else:
        p[0] = dict(args=p[2], kwargs=p[5])

def p_reference(p):
    """
    reference : identifier
              | expression '.' identifier
              | expression '[' expression ']'
              | expression '[' ':' expression ']'
              | expression '[' expression ':' ']'
              | expression '[' expression ':' expression ']'
    """
    if len(p) == 2:
        p[0] = Node(identifier=p[1])
    elif len(p) == 4:
        p[0] = Node(base=p[1], identifier=p[3])
    elif len(p) == 5:
        p[0] = Node(base=p[1], key=p[3])
    elif len(p) == 6:
        if p[3] == ':':
            p[0] = Node(base=p[1], right=p[4], node_type="slice")
        else:
            p[0] = Node(base=p[1], left=p[3], node_type="slice")
    else:
        p[0] = Node(base=p[1], left=p[3], right=p[5], node_type="slice")

def p_anonymousfunction(p):
    """
    anonymousfunction : FUNCTION '(' parameters ')' type '{' statement_list '}'
                      | FUNCTION '(' ')' type '{' statement_list '}'
    """
    if len(p) == 8:
        p[0] = Node(parameters=[], type=p[4], body=p[6])
    else:
        p[0] = Node(parameters=p[3], type=p[5], body=p[7])

def p_comprehension(p):
    """
    comprehension : '[' expression FOR '(' variable IN expression ')' ']'
                  | '[' expression FOR '(' variable IN expression ')' IF '(' expression ')' ']'
    """
    if len(p) > 8:
        p[0] = Node(expression=p[2], identifier=p[4], list_expression=p[6],
                    conditional_expression=p[8])
    else:
        p[0] = Node(expression=p[2], identifier=p[4], list_expression=p[6])

def p_list(p):
    """
    list : '[' expression_list ']'
         | '[' ']'
    """
    if len(p) == 3:
        p[0] = Node(values=[])
    else:
        p[0] = Node(values=p[2])

def p_yield(p):
    """
    yield : YIELD '(' call ')'
    """
    p[0] = Node(call=p[3])

def p_object(p):
    """
    object : '{' object_members '}'
    """
    p[0] = Node(members=p[2])

def p_object_members(p):
    """
    object_members : identifier ':' expression ',' object_members
                   | identifier ':' expression
                   |
    """
    if len(p) == 1:
        p[0] = []
    elif len(p) == 4:
        p[0] = [Node(name=p[1], value=p[3], node_type="objectmember")]
    else:
        p[0] = [Node(name=p[1], value=p[3], node_type="objectmember")] + p[5]

def p_type(p):
    """
    type : ':' identifier
         |
    """
    if len(p) > 1:
        p[0] = p[2]
    else:
        p[0] = None

def p_error(p):
    if p:
        logging.error("%s:%d: Syntax error at '%s'", PARSER.file_name,
                      p.lineno, p.value)
    else:
        logging.error("%s: Syntax error at EOF", PARSER.file_name)
    PARSER.num_errors += 1

PARSER = ply.yacc.yacc()
